Skip to content

prime-runs - #2415

Draft
kcoopermiller wants to merge 23 commits into
mainfrom
cooper/prime-runs
Draft

prime-runs#2415
kcoopermiller wants to merge 23 commits into
mainfrom
cooper/prime-runs

Conversation

@kcoopermiller

@kcoopermiller kcoopermiller commented Aug 20, 2026

Copy link
Copy Markdown
Member

Why

The eval CLI hand-rolled its own platform client: verifiers/v1/utils/platform.py was 317 lines of resolve env → create → post samples → finalize over raw httpx, run once after the last rollout. prime#856 moves that lifecycle into prime-runs, a leaf SDK (httpx + prime-traces) where a run is an object held for the length of the eval rather than three stateless calls at the end of it.

What

platform.py is now ~120 lines of wiring; the SDK owns transport, retries, backpressure and the terminal status:

run = open_run(config, push_state, num_examples=len(tasks))   # before the first rollout
if run.mode == "online":
    config.run.adopt_id(run.id)            # the platform's id is the run id

async def on_complete(episode):
    episode.record_run(EvalRunInfo(id=config.run.id, name=config.run.name))
    await append_episode(out, episode, write_lock)
    await asyncio.to_thread(run.log_episodes, [episode])   # streams as rollouts land

await asyncio.to_thread(finish_run, run, episodes, push_state)

Gone: push_traces, credentials, json_bytes, run_metrics, the batching loop, the payload ceiling, the in-memory buffer. trace_to_sample and build_samples moved to prime_runs.projection (knowledge about a platform wire format, not about an eval framework). build_samples stays importable from here because prime-rl's released monitor imports it across the repo boundary; nothing imports trace_to_sample.

file what changed
utils/platform.py open_run / finish_run / abort_run + PushState, the dashboard's read-through view of the run
cli/eval/runner.py both runners open the run before the first rollout, stream to it, and close it out on every exit path; gather_rollouts cancels sibling rollouts on the first failure so nothing keeps uploading into a run that is being closed
cli/eval/main.py drops the end-of-run push
configs/cli/eval.py RunConfig.adopt_id()
cli/dashboard/eval.py the footer reads the live run: dim Pushing traces (<url>) while it streams, white Traces pushed (<url>), red on failure
tests/v1/conftest.py the E2E fixture sets push=False
serve/client.py EnvClient.close() gathers only the still-pending cancel senders: on Python 3.12+ a gather over finished tasks completes without yielding, so the loop spun at 100% CPU on Ctrl-C once the runner started waiting for rollouts to unwind before teardown

What the run carries

Only what the pre-SDK upload sent as the evaluation's metadata: model, num_examples (the selected task count) and rollouts_per_example, the v0 keys the dashboard's lists and reproduce command read.

The config itself is deliberately not uploaded in this PR. The evaluations API is not beta-gated (only the Prime Traces sink is), and both a config dump and the launched @ eval.toml can carry credentials (client.headers, harness.env), so uploading either needs producer-side masking. That and the platform's rendering of it (platform#4740, which renders metadata.config_source verbatim) land together in a follow-up once #4740 is deployed. Keeping metadata.env a string here also means this PR does not depend on #4740.

Modes

--no-push keeps the run local. So does PRIME_RUNS_MODE=disabled, the SDK's own switch, honoured explicitly because open_run passes mode="online" and would otherwise override it. A local run keeps its own uuid rather than the SDK's disabled-… placeholder. A run that cannot be opened (no API key, hub resolution failure, a team outside an allowlist) falls back to local with the reason in the footer; the eval never fails because of the platform.

Blocked on

prime-runs on PyPI (prime#856). Until then the [tool.uv.sources] git pin (marked TEMPORARY) stays, published verifiers wheels are uninstallable with pip, and the sandbox-side installs in the live E2E cannot resolve prime-runs>=0.1.0, which is the current red E2E job. Once it is live: drop the pin, uv lock, and the E2E should go green.

Comment thread verifiers/v1/cli/eval/runner.py
Comment thread verifiers/v1/cli/dashboard/eval.py
Comment thread pyproject.toml
Comment thread verifiers/v1/cli/eval/runner.py Outdated
Comment thread verifiers/v1/cli/eval/runner.py
kcoopermiller and others added 10 commits August 21, 2026 15:34
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpNehr64sniVfqKo62GKPw
`run_config()` now carries what the dashboard reads unconditionally — `model`,
`num_examples` (the selected task count, passed in by the runner; -1 for "all"
when unknown) and `rollouts_per_example` — alongside the `exclude_unset` dump,
which drops defaults and so left the evals list showing "Unknown Model" and
"0 examples".

Credentials are stripped producer-side, so nothing sensitive reaches
`metadata`: every value under a `headers` table and any value whose key names
a credential (`api_key`, `Authorization`, `hf_token`; references such as
`api_key_var` are kept) is masked in the structured dump, and the launched
config file is uploaded verbatim only when parsing it (TOML/JSON) finds no
credential-holding key — otherwise, or when it cannot be inspected, it is left
out with a warning naming the offending paths.

`open_run` honours `PRIME_RUNS_MODE=disabled`, which the explicit
`mode="online"` used to override. A run that stays local keeps its own uuid
instead of adopting the SDK's `disabled-…` placeholder. The E2E fixture sets
`push=False`: `run_eval` opens a platform run before the first rollout when a
key is present, and CI has one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CyW4j57juAg5cheTX8PmUR
The only places an eval config can carry a credential are a client's
`headers` table and a seat's `harness.env` table: the API key itself is
`api_key_var` (an env-var name) and `forward_env` exists so harness secrets
need not be written down. Mask everything under those two tables and skip
the verbatim `config_source` when the file sets either, instead of guessing
from key names. Drops the credential-key regex, its reference-key
exemption list and the dotted-path scan.

Also widen `_close(summary=)` to `Mapping` so `metrics.RunSummary` (a
TypedDict) type-checks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDrFiuDUjdJrv8Qu4oud8C
…on't flip import order

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDEqDAXcNvbJ2GqsWttDhy
…ckouts don't flip import order"

This reverts commit cca4860.
kcoopermiller and others added 3 commits September 1, 2026 14:25
Upload only the three v0 metadata keys the dashboard reads (model,
num_examples, rollouts_per_example), as the pre-SDK push did. The
exclude_unset config dump and the launched file (config_source) are
deferred to a follow-up: both can carry credentials (client.headers,
harness.env), and the masking that made them safe was most of this
module. This also keeps metadata.env a string, so the evals list no
longer needs platform#4740 before this can ship.

Removed: scrub_secrets, credential_tables and their helpers,
RunConfig.source/record_source, and config_file_ref.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
Shorter docstrings and comments throughout; the PushState footer no
longer reports per-sink losses (the SDK logs them); PushState is always
constructed so the platform helpers take it unconditionally; run_config
is inlined into open_run with num_examples required; build_samples is
re-exported with a noqa instead of __all__.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
close() waited for in-flight cancel senders with
`while self._cancel_tasks: await gather(*self._cancel_tasks)`. On 3.12+
a gather over tasks that are all already done completes eagerly without
yielding to the loop, so the discard callbacks that empty the set never
run and the coroutine spins at 100% CPU. It was latent on main, where
close() is reached while the senders are still pending; the prime-runs
integration waits for the rollouts to unwind first, so every Ctrl-C on a
served eval hung. Gather only the pending tasks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bpEF1rKjCijybTYmfTGw2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant